EventBridgeからDiscordのエンドポイントを直接叩いて通知するしくみをCDKで実装してみました
こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。
監視と通知大事ですよね。
今回はCloudWatch Alarmでアラーム状態になったイベントについて、シンプルに整えてDiscordに送信してみました。
今回の構成
以下のようなシンプルな構成です。
ちょっと前まであれば、メッセージの整形・通知の部分にLambdaを使う選択肢が多かったと思います。
今回はEventBridgeの入力トランスフォーマーという機能を使って整形し、Discordに通知してみます。
たとえば、Slackの場合どんな方法があるか、こちらの記事に分かりやすくまとめられていたので興味のある方はご確認ください。
今回実装してみた環境は以下のとおりです。
Key | Value |
---|---|
OS | macOS Monterey |
node | v18.12.1 |
言語 | TypeScript |
cdk | v2.53.0 |
なお、今回紹介しているコードの全文は以下リポジトリに格納しています。
下準備: DiscordのWebhookを作成する
今回はDiscordへメッセージを投稿するためにWebhookを作成します。
投稿したいチャンネルの設定を開きます。
次にサイドバーの「連携サービス」をクリックして、「ウェブフックを作成」をクリックします。
作成されたら、「ウェブフックURLをコピー」をクリックしておきます。
このURLは次の項目で利用するため控えておいてください。
下準備: .envの作成
CDKを利用する上で環境変数や機密情報を取り扱う方法はいくつかあるかと思いますが、今回はWebhookのURLを誤って公開しないように.env
のしくみを利用しています。
contexnt.jsonを利用する方法などもありますので、気になる方は以下記事が分かりやすかったのでご参照ください。
CDKコードのルートディレクトリで以下のようにdotenv
をインストールしておきます。
npm i dotenv
また.env
というファイルを作成して、以下のように環境変数にWebhookのURLを設定しておきます。
ENDPOINT='https://discord.com/api/webhooks/hoge'
次に最も重要な手順ですが.gitignoreに.envを追加して、誤ってコミットしてしまうことを防ぎましょう。
*.env
次に環境変数を読み込むtsファイルのトップで以下のようにdotenvを準備します。
import * as dotenv from 'dotenv'; dotenv.config(); // 以下その他モジュールなどの読み込みのため割愛 import * as cdk from 'aws-cdk-lib';
Discord通知のためのCDKコードを書いてみた
今回は実際にEC2インスタンスを立ち上げて、CPU使用率を監視してみます。
以下のメトリクスの中から CPUUtilization
を監視します。
まずは、CDKで以下のようにサンプル用のEC2を立ち上げる準備をします。
export class EventbridgeDiscordStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // EC2作成 const vpc = new Vpc(this, 'Vpc', { cidr: '10.100.0.0/16', maxAzs: 2, natGateways: 0, subnetConfiguration: [ { cidrMask: 24, name: 'isolated', subnetType: SubnetType.PRIVATE_ISOLATED, }, ], }); const ami = new AmazonLinuxImage({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2, cpuType: AmazonLinuxCpuType.X86_64, }); const ec2 = new Instance(this, 'sampleInstance', { vpc: vpc, instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.MICRO), machineImage: ami, }); } }
次に、メトリクス監視のためのCloudWatchAlarmのリソースを作成します。
// 先ほどのEC2作成部分からの続き // CloudWatch Alarm const ec2CpuAlarm = new Alarm(this, 'testEC2CpuAlarm', { metric: new Metric({ namespace: 'AWS/EC2', metricName: 'CPUUtilization', dimensionsMap: { InstanceId: ec2.instanceId, }, statistic: 'Average', period: Duration.minutes(1), }), evaluationPeriods: 1, // 今回アラートのテストのため閾値を0.005%に指定 threshold: 0.005, comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, });
最後にこのAlarmを監視して、Discordに通知するためのリソースを作成します。
//EventBridge Rule const connection = new Connection(this, 'Connection', { authorization: Authorization.apiKey( 'Token if Nedded', // 今回特にTokenやAPI Keyは利用しない SecretValue.unsafePlainText('hoge') ), description: 'Connection with API Key Token If Needed', }); const destination = new ApiDestination(this, 'Destination', { connection, // 最初に取得したDiscordのWebhookエンドポイント endpoint: process.env.ENDPOINT ?? '', description: 'POST Discrod API', }); const rule = new Rule(this, 'testAlarmRule', { ruleName: 'testAlarmRule', eventPattern: { source: ['aws.cloudwatch'], detailType: ['CloudWatch Alarm State Change'], resources: [ec2CpuAlarm.alarmArn], }, }); rule.addTarget( new cdk.aws_events_targets.ApiDestination(destination, { // 入力トランスフォーマーでメッセージを整形 // https://aws.amazon.com/jp/premiumsupport/knowledge-center/eventbridge-human-readable-notifications/ event: RuleTargetInput.fromObject({ content: `:loudspeaker: アラート :loudspeaker: アラート名: ${EventField.fromPath('$.detail.alarmName')} アラート理由: ${EventField.fromPath('$.detail.state.reason')}`, }), }) );
なお、今回は利用していませんが、APIキーなどの機密情報を利用する場合、Secrets Managerに格納可能です。
その場合、Connection
の部分を以下のように変更するイメージです。
const secret = new Secret(this, 'Secret', { secretName: 'ChatWorkApiKey', generateSecretString: { generateStringKey: 'password', secretStringTemplate: JSON.stringify({ apiKey: process.env.APIKEY ?? '', }), }, }); const connection = new Connection(this, 'Connection', { authorization: Authorization.apiKey( 'X-ChatWorkToken', SecretValue.secretsManager(secret.secretArn, { jsonField: 'apiKey', }) ), description: 'Connection with API Key', });
通知のメッセージに関しては入力トランスフォーマーという機能を使って加工しています。
どのような設定で、どのようにメッセージが加工されているか確認するためデプロイしてみます。
cdk deploy
EC2インスタンスなどの必要リソースが作られて、しばらく待っていることで無事Discordに通知が届きました!
入力トランスフォーマーではEventBridgeに渡ってくる各サービスからの情報を加工しています。
各サービスからどのような構造のJSONでどのような情報があるかはこちらで確認できます。
例えばCloudWatch AlarmのページでJSON構造体が確認できます。
今回は以下のようなJSONがEventBridgeに渡されます。
{ "version": "0", "id": "c4c1c1c9-6542-e61b-6ef0-8c4d36933a92", "detail-type": "CloudWatch Alarm State Change", "source": "aws.cloudwatch", "account": "123456789012", "time": "2019-10-02T17:04:40Z", "region": "us-east-1", "resources": ["arn:aws:cloudwatch:us-east-1:123456789012:alarm:ServerCpuTooHigh"], "detail": { "alarmName": "ServerCpuTooHigh", "configuration": { "description": "Goes into alarm when server CPU utilization is too high!", "metrics": [{ "id": "30b6c6b2-a864-43a2-4877-c09a1afc3b87", "metricStat": { "metric": { "dimensions": { "InstanceId": "i-12345678901234567" }, "name": "CPUUtilization", "namespace": "AWS/EC2" }, "period": 300, "stat": "Average" }, "returnData": true }] }, "previousState": { "reason": "Threshold Crossed: 1 out of the last 1 datapoints [0.0666851903306472 (01/10/19 13:46:00)] was not greater than the threshold (50.0) (minimum 1 datapoint for ALARM -> OK transition).", "reasonData": "{\"version\":\"1.0\",\"queryDate\":\"2019-10-01T13:56:40.985+0000\",\"startDate\":\"2019-10-01T13:46:00.000+0000\",\"statistic\":\"Average\",\"period\":300,\"recentDatapoints\":[0.0666851903306472],\"threshold\":50.0}", "timestamp": "2019-10-01T13:56:40.987+0000", "value": "OK" }, "state": { "reason": "Threshold Crossed: 1 out of the last 1 datapoints [99.50160229693434 (02/10/19 16:59:00)] was greater than the threshold (50.0) (minimum 1 datapoint for OK -> ALARM transition).", "reasonData": "{\"version\":\"1.0\",\"queryDate\":\"2019-10-02T17:04:40.985+0000\",\"startDate\":\"2019-10-02T16:59:00.000+0000\",\"statistic\":\"Average\",\"period\":300,\"recentDatapoints\":[99.50160229693434],\"threshold\":50.0}", "timestamp": "2019-10-02T17:04:40.989+0000", "value": "ALARM" } } }
上のJSONから$.detail.alarmName
と$.detail.state.reason
を利用して、出力を整形しています。
{ "detail-alarmName": "$.detail.alarmName", "detail-state-reason": "$.detail.state.reason" }
{"content":":loudspeaker: アラート :loudspeaker:\nアラート名: <detail-alarmName>\nアラート理由: <detail-state-reason>"}
出力値はDiscordのメッセージ投稿APIの仕様に合わせています。
で以下の様な形でメッセージが届いている形です。
これで一通りの確認ができました。
再掲となりますが、コード全文をみたい場合は以下リポジトリをご確認ください。
まとめ
- 簡単な通知ならLambdaを組まないでもEventBridgeでDiscordに通知できた
- Discordに限らずさまざまなエンドポイントに応用ができる
なお、今回Chatworkへの通知も試してみたのですが、2022.12.5現在のChatworkの仕様で入力トランスフォーマーが利用できないようで、上記方法ではメッセージを加工できませんでした。
同じことをやる場合でも、さまざまなやり方が考えられて面白いですね。
これからもいろいろ検証していこうと思います!